盒子
盒子
文章目录
  1. QEMU-QTest && Libfuzzer源码分析(上)
    1. 0x01 TL;DR
    2. 0x02 SourceCode Version
    3. 0x03 Basic Principle
    4. 0x04 QTest Code Analyse
    5. 0x05 QTest Summary
    6. 0x06 Libfuzzer Test Code Analyze

QEMU源码分析 - QTest(上)

QEMU-QTest && Libfuzzer源码分析(上)

0x01 TL;DR

QEMU中的LibfuzzerAlexander Bulekov在19年Google SummerCode期间开发的一套QEMU内置的Fuzz工具。QEMU最开始代码测试的时候开发了一套名为QTest的测试工具,主要用它来编写测试用例。QEMU中的设备很多,一个一个去写相应设备的测试用例是很耗时间的,因此就有了在QTest结合Libfuzzer的测试工具。不过当我整体审计完QTest代码后,发现其实还是蛮有局限性的,待改进的空间也很大。

由于全文篇幅比较长,因此分为上下两篇叙述。

0x02 SourceCode Version

QEMU:5.2.0

0x03 Basic Principle

先简单梳理一下QTest的设计原理。我尽量保持原汁原味,不改变原意。

QTest大体将QEMU内容划分为四类:MachineDriverInterfaceTest,每一类为一个node(节点)。节点之间的关系被称作edgeedge分为三类:consumeproducecontain

关于edge官方解释是这样的(xynode):

1
2
3
x consumes y : x可以使用y(和produces对应)
x produces y : x给y提供接口
x contains y : y是x组件的一部分(x包含y)

QTest基本框架步骤如下:

  • 所有nodesedges都创建在各自的文件下 –> machine/driver/test
  • 启动QEMU后查询一系列的可用devicesmachines
  • 从可用的machines开始遍历并执行深度优先遍历,查询与test相应的情况。
  • 一旦遍历到了test,路径会重新走一遍并且所有drivers会被相应分配,最终的interface也会传给test
  • 执行test
  • 未被使用的对象会被清理以及路径发现(遍历)也会继续。

以下是一个编写新driver以及interface的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include "qgraph.h"

struct My_driver {
QOSGraphObject obj;
Node_produced prod;
Node_contained cont;
}

static void my_destructor(QOSGraphObject *obj)
{
g_free(obj);
}

static void my_get_driver(void *object, const char *interface) {
My_driver *dev = object;
if (!g_strcmp0(interface, "my_interface")) {
return &dev->prod;
}
abort();
}

static void my_get_device(void *object, const char *device) {
My_driver *dev = object;
if (!g_strcmp0(device, "my_driver_contained")) {
return &dev->cont;
}
abort();
}

static void *my_driver_constructor(void *node_consumed,
QOSGraphObject *alloc)
{
My_driver dev = g_new(My_driver, 1);
// get the node pointed by the produce edge
dev->obj.get_driver = my_get_driver;
// get the node pointed by the contains
dev->obj.get_device = my_get_device;
// free the object
dev->obj.destructor = my_destructor;
do_something_with_node_consumed(node_consumed);
// set all fields of contained device
init_contained_device(&dev->cont);
return &dev->obj;
}

static void register_my_driver(void)
{
qos_node_create_driver("my_driver", my_driver_constructor);
// contained drivers don't need a constructor,
// they will be init by the parent.
qos_node_create_driver("my_driver_contained", NULL);

// For the sake of this example, assume machine x86_64/pc contains
// "other_node".
// This relation, along with the machine and "other_node" creation,
// should be defined in the x86_64_pc-machine.c file.
// "my_driver" will then consume "other_node"
qos_node_contains("my_driver", "my_driver_contained");
qos_node_produces("my_driver", "my_interface");
qos_node_consumes("my_driver", "other_node");
}

上面这个例子里,所有可能的关系类型都创建了,具体关系如下:

1
2
3
4
5
x86_64/pc --contains--> other_node --consumed_by--> my_driver
|
my_driver_contained <--contains--+
|
my_interface <--produces--+

以下是编写一个新的test的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "qgraph.h"

static void my_test_function(void *obj, void *data)
{
Node_produced *interface_to_test = obj;
// test interface_to_test
}

static void register_my_test(void)
{
qos_add_test("my_interface", "my_test", my_test_function);
}

libqos_init(register_my_test);

新的test创建了,该testconsumemy_interface这个node,并且创建了一个有效的从machine到一个testpath。最终的图表会像下面这样:

1
2
3
4
5
x86_64/pc -->contains--> other_node --consumed_by--> my_driver
|
my_driver_contained <--contains--+
|
my_test <--consumed_by-- my_interface <--produces--+

假设有一个二进制文件QTEST_QEMU_BINARY=./qemu-system-x86_64,那么一个有效的test path就会像这样:/x86_64/pc/other_node/my_driver/my_interface/my_test

Command Line :

QEMU启动需要有一些Option参数,在QTest框架中的体现就是Command LineCommand Line是使用node names以及构建edges时通过用户传递的可选参数构建的。Command Line参数有三种类型:in nodeafter nodebefore node

1
2
3
4
5
in node: 根据node name创建,例如,machines会有“-M <machine>”传给command line,同时devices也会有“-device <device>”,该框架会自动完成创建。

after node: 以额外参数添加在node name中。当创建edges时该参数的添加是可选的,通过设置#QOSGraphEdgeOptions结构体中的@after_cmd_line和@extra_edge_opts属性即可。框架也会在@extra_edge_opts之前自动添加一个段落,因为这会在edge包含的options所指向的目标node之后添加属性,并且自动在@after_cmd_line之前添加一个空格,因为这是添加一个额外的device,并不是添加一个额外的属性。

before node: 以额外参数添加在node name中。当创建edges时也是可选的,通过设置#QOSGraphEdgeOptions结构体中的@before_cmd_line即可。这个属性会在edge包含的options所指向的目标node之前添加属性。这对于不是节点可视的命令来说很有用,例如“-fdsev”、“-netdev”。

尽管Command Lineedges中总会被使用,但不是所有的nodes names在没一个路径遍历(path walk)中会被用到。因为contained或者produced关系总会被QEMU添加,因此只有consumes会被用在建立Command Line中。

使用例子如下:

1
2
3
4
5
6
7
8
9
QOSGraphEdgeOptions opts = {
.arg = NULL,
.size_arg = 0,
.after_cmd_line = "-device other",
.before_cmd_line = "-netdev something",
.extra_edge_opts = "addr=04.0",
};
QOSGraphNode * node = qos_node_create_driver("my_node", constructor);
qos_node_consumes_args("my_node", "interface", &opts);

最终构造出来的Command Line如下:

1
-netdev something -device my_node,addr=04.0 -device other

QOSGraphEdgeOptions结构体如下:

1
2
3
4
5
6
7
8
struct QOSGraphEdgeOptions {
void *arg;
uint32_t size_arg;
const char *extra_device_opts;//会自动添加“,”
const char *before_cmd_line; //例如“-fdsev synt”
const char *after_cmd_line; //用于添加其他设备命令,自动添加“ ”空格
const char *edge_name; //用于区分多个设备有同样的node name
};

接下来就说几个比较重要的函数:

  • qos_node_contains
1
2
void qos_node_contains(const char *container, const char *contained,
QOSGraphEdgeOptions *opts, ...);

用来创建一个或多个edgestype类型为QEDGE_CONTAINS。如果@opts为空,那么只会创建一个没有options的单条edge,如果不空,每个option都会创建一条edge。这个函数对于在同个machine node下有着相同node names的多个设备来说很有用。例如,arm/raspi2包含了两个generic-sdhci设备,正确的命令会是这样:

1
2
3
4
5
6
qos_node_create_machine("arm/raspi2");
qos_node_create_driver("generic-sdhci", constructor);
//assume rest of the fields are set NULL
QOSGraphEdgeOptions op1 = { .edge_name = "emmc" };
QOSGraphEdgeOptions op2 = { .edge_name = "sdcard" };
qos_node_contains("arm/raspi2", "generic-sdhci", &op1, &op2, NULL);

当然这也需要@container(包含者)的get_device函数对emmcsdcard都有一个实现实例。

op1.argop1.size_arg代表传递给@contained(被包含者)构造函数的参数用于正确的初始化。

  • qos_add_test
1
2
3
void qos_add_test(const char *name, const char *interface,
QOSTestFunc test_func,
QOSGraphTestOptions *opts);

用于添加test node,该testconsume一个interface node,一旦图表的遍历算法找到了这个测试路径,@test_func就会被执行。对于test node来说,opts->edge.argsize_arg代表传递给@test_func的参数。

简单总结 :

QTest框架主要围绕两类nodeedge来展开,两类结构体以及两类的type类型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/* edge types*/
enum QOSEdgeType {
QEDGE_CONTAINS,
QEDGE_PRODUCES,
QEDGE_CONSUMED_BY
};

/* node types*/
enum QOSNodeType {
QNODE_MACHINE,
QNODE_DRIVER,
QNODE_INTERFACE,
QNODE_TEST
};

/* Graph Node */
struct QOSGraphNode {
QOSNodeType type;
bool available; /* set by QEMU via QMP, used during graph walk */
bool visited; /* used during graph walk */
char *name; /* used to identify the node */
char *command_line; /* used to start QEMU at test execution */
union {
struct {
QOSCreateDriverFunc constructor;
} driver;
struct {
QOSCreateMachineFunc constructor;
} machine;
struct {
QOSTestFunc function;
void *arg;
QOSBeforeTest before;
bool subprocess;
} test;
} u;

/**
* only used when traversing the path, never rely on that except in the
* qos_traverse_graph callback function
*/
QOSGraphEdge *path_edge;
};

/* Graph Edge */
struct QOSGraphEdge {
QOSEdgeType type;
char *dest;
void *arg; /* just for QEDGE_CONTAINS
* and QEDGE_CONSUMED_BY */
char *extra_device_opts; /* added to -device option, "," is
* automatically added
*/
char *before_cmd_line; /* added before node cmd_line */
char *after_cmd_line; /* added after -device options */
char *edge_name; /* used by QEDGE_CONTAINS */
QSLIST_ENTRY(QOSGraphEdge) edge_list;
};

其他的例如QOSGraphEdgeOptionsQOSGraphTestOptions实际上是nodeedge的一个拓展延伸(参数选项),最终还是要赋值到nodeedge中去的。

值得一提的还有QOSGraphObject,该结构体是用于testdrivermachine的实例化作为他们的第一个字段(域)。定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct QOSGraphObject {
/* for produces edges, returns void * */
QOSGetDriver get_driver;
/* for contains edges, returns a QOSGraphObject * */
QOSGetDevice get_device;
/* start the hw, get ready for the test */
QOSStartFunct start_hw;
/* destroy this QOSGraphObject */
QOSDestructorFunc destructor;
/* free the memory associated to the QOSGraphObject and its contained
* children */
GDestroyNotify free;
};

0x04 QTest Code Analyse

后续部分建议边调试(审计)边食用。先主要分析QTest相关的代码,之后再来看libfuzzer部分,QTest部分主要逻辑在qos-test.c文件中。main函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
qos_graph_init(); //建立graph图标,初始化node和edge的表,并创建一个
//名为ROOT的”根“node节点,类型为driver
module_call_init(MODULE_INIT_QOM);
module_call_init(MODULE_INIT_LIBQOS);
qos_set_machines_devices_available();

qos_graph_foreach_test_path(walk_path);
g_test_run();
qtest_end();
qos_graph_destroy();
g_free(old_path);
return 0;
}

module_call_init值得一说,MODULE_INIT_QOM是在type_init(function)中指定的类型,具体为module_init(function, MODULE_INIT_QOM),看定义可以得知它是一个构造函数,在QEMU运行之前就执行了,具体操作为:

1
2
3
4
5
6
7
8
9
10
11
12
13
void register_module_init(void (*fn)(void), module_init_type type)
{
ModuleEntry *e;
ModuleTypeList *l;

e = g_malloc0(sizeof(*e));
e->init = fn; //赋值type_init中的函数给ModuleEntry->init
e->type = type; //赋值type

l = find_type(type);

QTAILQ_INSERT_TAIL(l, e, node); //插入TypeList链表
}

moudle_call_init函数为:

1
2
3
4
5
6
7
8
void module_call_init(module_init_type type)
{
//.......
QTAILQ_FOREACH(e, l, node) {
e->init(); //执行init函数
}
//.......
}

type_init出现在QEMU设备代码中,用于设备的注册/初始化。所以module_call_init(MODULE_INIT_QOM)就是拿来初始化QEMU中的设备的。同样的,module_call_init(MODULE_INIT_LIBQOS)用于初始化libqos框架,把QTest中的machinedrivertest等都初始化了,具体可以查看调用了libqos_init(function)函数的代码文件。

qos_set_machines_devices_available()作用是将machinesdevicesnode->availabe设为true,默认创建node的时候是false。相当于启用设备,为后续的path walk做准备。

接下来就是重点要分析的“路径遍历”了。首先看qos_graph_foreach_test_path函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void qos_graph_foreach_test_path(QOSTestCallback fn)
{
QOSGraphNode *root = qos_graph_get_node(QOS_ROOT); //获取“根”ROOT节点
qos_traverse_graph(root, fn);
}

static void qos_traverse_graph(QOSGraphNode *root, QOSTestCallback callback)
{
QOSGraphNode *v, *dest_node, *path;
QOSStackElement *s_el;
QOSGraphEdge *e, *next;
QOSGraphEdgeList *list;

qos_push(root, NULL, NULL); //将“根”node压栈,无父节点

while (qos_node_tos > 0) { //代表栈的深度,指向栈顶+1
s_el = qos_tos(); //取出栈顶元素
v = s_el->node;
if (v->visited) { //若被遍历访问过了,则出栈并设置unvisited
qos_pop();
continue;
}
v->visited = true;
list = get_edgelist(v->name); //获取node对应的edge链表
if (!list) { //若无链表则出栈并设置unvisited
qos_pop();
if (v->type == QNODE_TEST) { //若为test节点,则回溯整条路径,并执行回调函数
v->visited = false;
path = qos_reverse_path(s_el);
callback(path, s_el->length);
}
} else {
QSLIST_FOREACH_SAFE(e, list, edge_list, next) { //逐个遍历edge链表
dest_node = search_node(e->dest); //查找目标edge节点

if (!dest_node) {
fprintf(stderr, "node %s in %s -> %s does not exist\n",
e->dest, v->name, e->dest);
abort();
}

if (!dest_node->visited && dest_node->available) { //若节点没被访问过,且可用,则
//压栈(包括父节点元素、edge
//等)
qos_push(dest_node, s_el, e);
}
}
}
}
}

qos_traverse_graph是路径遍历的算法函数,采用“栈”的方式来操作。定义了一个名为qos_node_stack的栈元素数组,数组中的每个元素为一个叫QOSStackElement的结构体,包含了nodeparent以及两者间的edge

1
2
3
4
5
6
7
8
struct QOSStackElement {
QOSGraphNode *node;
QOSStackElement *parent;
QOSGraphEdge *parent_edge;
int length;
};

static QOSStackElement qos_node_stack[QOS_PATH_MAX_ELEMENT_SIZE]; //QOS_PATH_MAX_ELEMENT_SIZE = 50

深度最长为50。

我作了一个节点树的图,来表明当前QEMU中节点之间的联系:

节点树

我只列出了部分node,主要做个效果,审计该函数的时候可以对照这个节点树来看,从上图可以找到一条完整的通路:

ROOT –> i386/pc –> i440FX-pcihost –> pci-bus-pc –> pci-bus –> virtio-scsi-pci –> virtio-scsi –> hotplug

这就是一条从machinetest的完整通路。当然,这只是其中一条,还有许多条test路径。

同时,我也作了一个该函数利用栈来操作的栈空间示意图,如下:

栈空间示意图

一起结合着看,该函数的意图就显而易见了,就是深度优先遍历的算法。

算法看完了,再来看该函数处理路径的部分:

1
2
3
4
5
6
7
8
if (!list) {                                //若无链表则出栈并设置unvisited
qos_pop();
if (v->type == QNODE_TEST) { //若为test节点,则回溯整条路径,并执行回调函数
v->visited = false;
path = qos_reverse_path(s_el);
callback(path, s_el->length);
}
} else {

当遍历到一条完整的machinetest路径时,就开始做处理了。qos_reverse_path(s_el)函数简单说一下就是对node结构体的path_edge做操作,链接这条路径的各个父子节点,回溯出这条链路上的所有nodeedge关系。callback函数就是传入的walk_path函数,参数为paths_el->length,也就是遍历到的路径和路径的深度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
static void walk_path(QOSGraphNode *orig_path, int len)
{
QOSGraphNode *path;
QOSGraphEdge *edge;

/* etype set to QEDGE_CONSUMED_BY so that machine can add to the command line */
QOSEdgeType etype = QEDGE_CONSUMED_BY; //默认设consumed_by为了让-M <machine>能添加到cmd上

/* twice QOS_PATH_MAX_ELEMENT_SIZE since each edge can have its arg */
char **path_vec = g_new0(char *, (QOS_PATH_MAX_ELEMENT_SIZE * 2));
int path_vec_size = 0;

char *after_cmd, *before_cmd, *after_device;
GString *after_device_str = g_string_new("");
char *node_name = orig_path->name, *path_str;

GString *cmd_line = g_string_new("");
GString *cmd_line2 = g_string_new("");

path = qos_graph_get_node(node_name); /* root */ //获取“ROOT”根节点
node_name = qos_graph_edge_get_dest(path->path_edge); //获取根节点的子节点name(machine节点)

path_vec[path_vec_size++] = node_name;
path_vec[path_vec_size++] = qos_get_machine_type(node_name); //根据name获取machine类型(pc/rasp等)

for (;;) {
path = qos_graph_get_node(node_name); //根据name获取节点
if (!path->path_edge) { //判断该节点是否还存在子节点,不存在则退出
break;
}

node_name = qos_graph_edge_get_dest(path->path_edge); //获取该节点的子节点name

/* append node command line + previous edge command line */
if (path->command_line && etype == QEDGE_CONSUMED_BY) { //如果节点有参数且为consumed
//by类型则添加cmd命令
g_string_append(cmd_line, path->command_line);
g_string_append(cmd_line, after_device_str->str);
g_string_truncate(after_device_str, 0);
}

//赋值子节点的edge_name(基本和dest一样,为了区分同个父节点的两个节点)
path_vec[path_vec_size++] = qos_graph_edge_get_name(path->path_edge);
/* detect if edge has command line args */
//获取before、after、after_device的command参数
after_cmd = qos_graph_edge_get_after_cmd_line(path->path_edge);
after_device = qos_graph_edge_get_extra_device_opts(path->path_edge);
before_cmd = qos_graph_edge_get_before_cmd_line(path->path_edge);
//获取与子节点间的edge关系
edge = qos_graph_get_edge(path->name, node_name);
//获取edge关系的type类型
etype = qos_graph_edge_get_type(edge);

if (before_cmd) { //将前面取的command参数添加赋值
g_string_append(cmd_line, before_cmd);
}
if (after_cmd) {
g_string_append(cmd_line2, after_cmd);
}
if (after_device) {
g_string_append(after_device_str, after_device);
}
}

path_vec[path_vec_size++] = NULL;
g_string_append(cmd_line, after_device_str->str);
g_string_free(after_device_str, true);

g_string_append(cmd_line, cmd_line2->str); //最终结合完成before、after、after_device的
//总体command参数
g_string_free(cmd_line2, true);

/* here position 0 has <arch>/<machine>, position 1 has <machine>.
* The path must not have the <arch>, qtest_add_data_func adds it.
*/
path_str = g_strjoinv("/", path_vec + 1);

/* put arch/machine in position 1 so run_one_test can do its work
* and add the command line at position 0.
*/
path_vec[1] = path_vec[0];
path_vec[0] = g_string_free(cmd_line, false);

if (path->u.test.subprocess) { //判断test是否有子进程
gchar *subprocess_path = g_strdup_printf("/%s/%s/subprocess",
qtest_get_arch(), path_str);
qtest_add_data_func(path_str, subprocess_path, subprocess_run_one_test);
g_test_add_data_func(subprocess_path, path_vec, run_one_test);
} else {
qtest_add_data_func(path_str, path_vec, run_one_test);
}

g_free(path_str);
}

path_str指向一连串的字符串(例如”pc/i440FX-pcihost/...“),path_vec指向一个字符串数组(例如[0] = "i386/pc" [1] = "pc"...

后续就开始执行相应的test函数run_one_test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void run_one_test(const void *arg)
{
QOSGraphNode *test_node;
QGuestAllocator *alloc = NULL;
void *obj;
char **path = (char **) arg;
GString *cmd_line = g_string_new(path[0]); //获取command参数
void *test_arg;

/* Before test */
current_path = path;
test_node = qos_graph_get_node(path[(g_strv_length(path) - 1)]); //获取test node
test_arg = test_node->u.test.arg; //获取test arg
if (test_node->u.test.before) { //如果有before函数则执行
test_arg = test_node->u.test.before(cmd_line, test_arg);
}

restart_qemu_or_continue(cmd_line->str); //比较和前一次启动cmd是否一样,不一样就重启
g_string_free(cmd_line, true);

obj = qos_allocate_objects(global_qtest, &alloc); // !!!
test_node->u.test.function(obj, test_arg, alloc); //执行test函数
}

其中qos_allocate_objects函数是颇为重要的一部分,这部分就是测试函数最关键的一点–“对象”。之前所获取的都是一些node节点和edge关系,这只是一个很抽象代表的东西,并没有实例化,也就是说,没有实际设备上的结构体,例如一个设备所包含的一些功能或者元素属性。因此,我们需要实例化,来为后续测试函数做准备。先看看这个qos_allocate_objects函数具体做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
void *qos_allocate_objects(QTestState *qts, QGuestAllocator **p_alloc)
{
//current_path+1为[0]=i386/pc [1]=i440FX-pcihost [..]=test_node,即machine到test的路径数组
return allocate_objects(qts, current_path + 1, p_alloc);
}

void *allocate_objects(QTestState *qts, char **path, QGuestAllocator **p_alloc)
{
int current = 0;
QGuestAllocator *alloc;
QOSGraphObject *parent = NULL;
QOSGraphEdge *edge;
QOSGraphNode *node;
void *edge_arg;
void *obj;

node = qos_graph_get_node(path[current]); //先获取machine node(i386/pc)
g_assert(node->type == QNODE_MACHINE);

obj = qos_machine_new(node, qts); //执行machine的constructor函数 (1)
qos_object_queue_destroy(obj); //设置销毁函数

alloc = get_machine_allocator(obj); //获取前面所设置的内存空间
if (p_alloc) {
*p_alloc = alloc;
}

for (;;) { // (2)
if (node->type != QNODE_INTERFACE) { //判断是否为interface node
qos_object_start_hw(obj); //执行start_hw函数
parent = obj;
}

/* follow edge and get object for next node constructor */
current++;
edge = qos_graph_get_edge(path[current - 1], path[current]); //获取与下一个node的edge
node = qos_graph_get_node(path[current]); //获取下一个node

if (node->type == QNODE_TEST) { //判断下个node是否为test,是则退出并返回obj
g_assert(qos_graph_edge_get_type(edge) == QEDGE_CONSUMED_BY);
return obj;
}

switch (qos_graph_edge_get_type(edge)) { //判断与下个node的edge
case QEDGE_PRODUCES:
obj = parent->get_driver(parent, path[current]); //为produce,则获取实例化的interface node
break;

case QEDGE_CONSUMED_BY: //为consumed_by,则执行driver->constructor初始化并获取obj
edge_arg = qos_graph_edge_get_arg(edge);
obj = qos_driver_new(node, obj, alloc, edge_arg);
qos_object_queue_destroy(obj);
break;

case QEDGE_CONTAINS: //为contain,则获取实例化的driver node
obj = parent->get_device(parent, path[current]);
break;
}
}
}

标记1处,拿i386/pc下的架构来说,它的constructor函数是x86_64_pc-machine.c文件中的qos_create_machine_pc()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void *qos_create_machine_pc(QTestState *qts)
{ //申请结构体空间,并设置获取device和interface的函数
QX86PCMachine *machine = g_new0(QX86PCMachine, 1);
machine->obj.get_device = pc_get_device;
machine->obj.get_driver = pc_get_driver;
machine->obj.destructor = pc_destructor;
pc_alloc_init(&machine->alloc, qts, ALLOC_NO_FLAGS); // !!!
qos_create_i440FX_host(&machine->bridge, qts, &machine->alloc);

return &machine->obj;
}

void pc_alloc_init(QGuestAllocator *s, QTestState *qts, QAllocOpts flags)
{
//....
ram_size = qfw_cfg_get_u64(fw_cfg, FW_CFG_RAM_SIZE);
alloc_init(s, flags, 1 << 20, MIN(ram_size, 0xE0000000), PAGE_SIZE);
//....
}//主要用于获取QEMU的内存空间,取ram_size和0xE0000000的最小值,赋值给QGuestAllocator

标记2处往下,就是循环遍历machine nodetest node的过程,并在每次遍历的过程当中,都做obj初始化分配的工作以及结构体初始化填充的工作,使得各个node的实例化obj也都能相互链接起来。

结构体上的链接关系如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//每个结构体都包含着QOSGraphObject,目的就是能够自顶向下链接起来
//最顶端的machine结构体
struct QX86PCMachine {//machine node
QOSGraphObject obj;
QGuestAllocator alloc;
i440FX_pcihost bridge; //包含了i440FX的结构体
};

struct i440FX_pcihost {//driver node
QOSGraphObject obj;
QPCIBusPC pci; //包含了PCIBusPC结构体
};

typedef struct QPCIBusPC {//driver node
QOSGraphObject obj;
QPCIBus bus; //包含了PCIBus结构体
} QPCIBusPC;

//如果是interface node的话(例如pci-bus是pci-bus-pc produce的),则后续就不包含一些结构体,而只提供一
//些接口。其他driver需要使用该接口的话就会相应的调用。以pci-bus为例。
struct QPCIBus {//interface node
uint8_t (*pio_readb)(QPCIBus *bus, uint32_t addr);
uint16_t (*pio_readw)(QPCIBus *bus, uint32_t addr);
uint32_t (*pio_readl)(QPCIBus *bus, uint32_t addr);
uint64_t (*pio_readq)(QPCIBus *bus, uint32_t addr);

void (*pio_writeb)(QPCIBus *bus, uint32_t addr, uint8_t value);
void (*pio_writew)(QPCIBus *bus, uint32_t addr, uint16_t value);
void (*pio_writel)(QPCIBus *bus, uint32_t addr, uint32_t value);
void (*pio_writeq)(QPCIBus *bus, uint32_t addr, uint64_t value);

void (*memread)(QPCIBus *bus, uint32_t addr, void *buf, size_t len);
void (*memwrite)(QPCIBus *bus, uint32_t addr, const void *buf, size_t len);

uint8_t (*config_readb)(QPCIBus *bus, int devfn, uint8_t offset);
uint16_t (*config_readw)(QPCIBus *bus, int devfn, uint8_t offset);
uint32_t (*config_readl)(QPCIBus *bus, int devfn, uint8_t offset);

void (*config_writeb)(QPCIBus *bus, int devfn,
uint8_t offset, uint8_t value);
void (*config_writew)(QPCIBus *bus, int devfn,
uint8_t offset, uint16_t value);
void (*config_writel)(QPCIBus *bus, int devfn,
uint8_t offset, uint32_t value);

QTestState *qts;
uint16_t pio_alloc_ptr;
uint64_t mmio_alloc_ptr, mmio_limit;
bool has_buggy_msi; /* TRUE for spapr, FALSE for pci */

};

allocate_objects函数的之后部分,整个循环结束之后,得到的obj就是test node所使用的interface nodeobj

重新回到run_one_test函数中来:

1
2
3
4
5
6
static void run_one_test(const void *arg)
{
//......
obj = qos_allocate_objects(global_qtest, &alloc); // !!!
test_node->u.test.function(obj, test_arg, alloc); //执行test函数
}

最终就是执行相应的test function就结束了。

0x05 QTest Summary

至此,整个qos-test.cQTest细节就结束了,后续的话就是一些test测试文件,这些留给读者自行阅读。大致概括一下QTest的整体流程就是每找到machinetest的路径,就执行相应的test function,相当于把所有编写的test测试用例都执行了一遍。他不是一个持续性测试(持续性喂各种不同的数据进行测试)的过程,而是“一次性”的测试,因此局限性显而易见。

同时,编写的test是基于前者interface提供的接口进行的,所以测试情况会受interface的接口限制。如果没有想要测试的设备的接口,那么完全就没有办法编写相应的test

0x06 Libfuzzer Test Code Analyze

先了解一下作者对fuzz target所设计的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef struct FuzzTarget {
const char *name; //唯一标识符 (用于传参给--fuzz-target=)
const char *description; //描述符,帮助理解

//返回的参数会用于QEMU初始化工作中
GString *(*get_init_cmdline)(struct FuzzTarget *);

//只执行一次,并且在QEMU初始化之前执行(例如设置共享内存用于和子进程交互),也可以为空
void(*pre_vm_init)(void);

//只执行一次,在QEMU已经初始化之后执行,但在fuzz循环开始之前执行(例如检测内存映射情况),可为空
void(*pre_fuzz)(QTestState *);

//从libfuzzer中获取输入并执行,为fuzz的主函数,需要处理设置、输入执行以及清理。不能为空
void(*fuzz)(QTestState *, const unsigned char *, size_t);

//用于引导fuzz过程中的变异结合情况,提高代码覆盖率。从种子集中取两个输入,分别有第一个输入的data和长度
//以及第二个输入的data和长度,out代表存储变异结果的空间,max_out_size代表结果的最大存储长度,seed
//必要时用于使得变异更具有确定性。上述具体的内容可以查看官方的LLVMFuzzerCustomCrossOver API文档
size_t(*crossover)(const uint8_t *data1, size_t size1,
const uint8_t *data2, size_t size2,
uint8_t *out, size_t max_out_size,
unsigned int seed);

void *opaque;
} FuzzTarget;

集成libfuzzer这块的代码存在于/tests/qtest/fuzz目录下,主代码文件是fuzz.c,先看前期init的函数LLVMFuzzerInitialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int LLVMFuzzerInitialize(int *argc, char ***argv, char ***envp)
{
//......
module_call_init(MODULE_INIT_FUZZ_TARGET); //(1) 初始化fuzz target
//......

qemu_init_exec_dir(**argv);
target_name = strstr(**argv, "-target-");

//......

fuzz_qtest_set_serialize(serialize); //(2) 设置是否查看qtest command命令模式
/* Identify the fuzz target */
fuzz_target = fuzz_get_target(target_name); //(3) 获取某个特定的fuzz target
if (!fuzz_target) {
usage(**argv);
}

fuzz_qts = qtest_setup(); //(4) 初始化qtest中基础QTestState的收发等操作函数

if (fuzz_target->pre_vm_init) {
fuzz_target->pre_vm_init();
}

//后续就是设置QEMU的启动参数,并初始化QEMU启动模块
GString *cmd_line = fuzz_target->get_init_cmdline(fuzz_target);
g_string_append_printf(cmd_line, " %s -qtest /dev/null ",
getenv("QTEST_LOG") ? "" : "-qtest-log none");

/* Split the runcmd into an argv and argc */
wordexp_t result;
wordexp(cmd_line->str, &result, 0);
g_string_free(cmd_line, true);

qemu_init(result.we_wordc, result.we_wordv, NULL);

//......
return 0;
}

标记1处和前面分析QTest的时候提到的module_call_init(MODULE_INIT_QOM)类似。实际上就是执行fuzz_target_init();调用的函数,拿i440fx_fuzz.c举例,其中最底部有这么一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void register_pci_fuzz_targets(void)
{
//......
fuzz_add_qos_target(&(FuzzTarget){
.name = "i440fx-qos-noreset-fuzz",
.description = "Fuzz the i440fx using raw qtest commands and "
"rebooting after each run",
.fuzz = i440fx_fuzz_qos,},
"i440FX-pcihost",
&(QOSGraphTestOptions){}
);
}

fuzz_target_init(register_pci_fuzz_targets);

调用module_call_init就是执行register_pci_fuzz_targets函数,初始化编写的fuzz target。当然,不止这一个文件调用了fuzz_target_init函数。

标记2处的作用是使得传输的QTest指令显式表示,也就是能够从command上看出传输的地址以及数据,但是这样会消耗部分资源,因此默认是关闭的。

标记3的具体函数如下:

1
2
3
4
5
6
7
8
9
10
static FuzzTarget *fuzz_get_target(char* name)
{
//......
QSLIST_FOREACH(tmp, fuzz_target_list, target_list) { //循环处理链条上的每个fuzz target
if (strcmp(tmp->target->name, name) == 0) { //对比输入的name和target name
return tmp->target;
}
}
return NULL;
}

很明显就是取出对应namefuzz target

标记4处具体函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static QTestState *qtest_setup(void)
{
qtest_server_set_send_handler(&qtest_client_inproc_recv, &fuzz_qts);
return qtest_inproc_init(&fuzz_qts, false, fuzz_arch,
&qtest_server_inproc_recv);
}

void qtest_server_set_send_handler(void (*send)(void*, const char*),
void *opaque)
{
qtest_server_send = send;
qtest_server_send_opaque = opaque;
}

static void qtest_send(CharBackend *chr, const char *str) //该函数会调用上面的server_send函数
{
qtest_server_send(qtest_server_send_opaque, str);
}



QTestState *qtest_inproc_init(QTestState **s, bool log, const char* arch,
void (*send)(void*, const char*))
{
//......
qtest_client_set_rx_handler(qts, qtest_client_inproc_recv_line); //s->ops.recv_line赋值

/* send() may not have a matching protoype, so use a type-safe wrapper */
qts->ops.external_send = send;
qtest_client_set_tx_handler(qts, send_wrapper); // s->ops.send赋值

//......
gchar *bin_path = g_strconcat("/qemu-system-", arch, NULL);
setenv("QTEST_QEMU_BINARY", bin_path, 0);
g_free(bin_path);

return qts;
}

基本就是一些初始化QTestState结构体的工作,包括一些sendrecv操作函数的赋值等等。

接下来就是正式开始执行fuzz的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int LLVMFuzzerTestOneInput(const unsigned char *Data, size_t Size)
{
/*
* Do the pre-fuzz-initialization before the first fuzzing iteration,
* instead of before the actual fuzz loop. This is needed since libfuzzer
* may fork off additional workers, prior to the fuzzing loop, and if
* pre_fuzz() sets up e.g. shared memory, this should be done for the
* individual worker processes
*/
static int pre_fuzz_done;
if (!pre_fuzz_done && fuzz_target->pre_fuzz) {
fuzz_target->pre_fuzz(fuzz_qts); //首先执行fuzz之前需要执行的函数
pre_fuzz_done = true;
}

fuzz_target->fuzz(fuzz_qts, Data, Size); //正式启动循环fuzz
return 0;
}

其余的一些函数就比较易懂了,留给读者自行分析。接下来所要说的就是我觉得该作者在集成libfuzzer过程中做的最棒的一点–设计了一个generic-fuzzer,也就是一个通用fuzzer,能够fuzz QEMU中的任何设备。后续的分析请看(下)篇。

支持一下
扫一扫,支持v1nke
  • 微信扫一扫
  • 支付宝扫一扫